/*
 * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package javax.swing.text.rtf;

import java.lang.*;
import java.util.*;
import java.io.*;
import java.awt.Color;
import java.security.AccessController;
import java.security.PrivilegedAction;
import javax.swing.text.*;

/**
 * Takes a sequence of RTF tokens and text and appends the text
 * described by the RTF to a <code>StyledDocument</code> (the <em>target</em>).
 * The RTF is lexed
 * from the character stream by the <code>RTFParser</code> which is this class's
 * superclass.
 *
 * This class is an indirect subclass of OutputStream. It must be closed
 * in order to guarantee that all of the text has been sent to
 * the text acceptor.
 *
 * @see RTFParser
 * @see java.io.OutputStream
 */
class RTFReader extends RTFParser {

  /**
   * The object to which the parsed text is sent.
   */
  StyledDocument target;

  /**
   * Miscellaneous information about the parser's state. This
   * dictionary is saved and restored when an RTF group begins
   * or ends.
   */
  Dictionary<Object, Object> parserState;   /* Current parser state */
  /**
   * This is the "dst" item from parserState. rtfDestination
   * is the current rtf destination. It is cached in an instance
   * variable for speed.
   */
  Destination rtfDestination;
  /**
   * This holds the current document attributes.
   */
  MutableAttributeSet documentAttributes;

  /**
   * This Dictionary maps Integer font numbers to String font names.
   */
  Dictionary<Integer, String> fontTable;
  /**
   * This array maps color indices to Color objects.
   */
  Color[] colorTable;
  /**
   * This array maps character style numbers to Style objects.
   */
  Style[] characterStyles;
  /**
   * This array maps paragraph style numbers to Style objects.
   */
  Style[] paragraphStyles;
  /**
   * This array maps section style numbers to Style objects.
   */
  Style[] sectionStyles;

  /**
   * This is the RTF version number, extracted from the \rtf keyword.
   * The version information is currently not used.
   */
  int rtfversion;

  /**
   * <code>true</code> to indicate that if the next keyword is unknown,
   * the containing group should be ignored.
   */
  boolean ignoreGroupIfUnknownKeyword;

  /**
   * The parameter of the most recently parsed \\ucN keyword,
   * used for skipping alternative representations after a
   * Unicode character.
   */
  int skippingCharacters;

  static private Dictionary<String, RTFAttribute> straightforwardAttributes;

  static {
    straightforwardAttributes = RTFAttributes.attributesByKeyword();
  }

  private MockAttributeSet mockery;

  /* this should be final, but there's a bug in javac... */
  /**
   * textKeywords maps RTF keywords to single-character strings,
   * for those keywords which simply insert some text.
   */
  static Dictionary<String, String> textKeywords = null;

  static {
    textKeywords = new Hashtable<String, String>();
    textKeywords.put("\\", "\\");
    textKeywords.put("{", "{");
    textKeywords.put("}", "}");
    textKeywords.put(" ", "\u00A0");  /* not in the spec... */
    textKeywords.put("~", "\u00A0");  /* nonbreaking space */
    textKeywords.put("_", "\u2011");  /* nonbreaking hyphen */
    textKeywords.put("bullet", "\u2022");
    textKeywords.put("emdash", "\u2014");
    textKeywords.put("emspace", "\u2003");
    textKeywords.put("endash", "\u2013");
    textKeywords.put("enspace", "\u2002");
    textKeywords.put("ldblquote", "\u201C");
    textKeywords.put("lquote", "\u2018");
    textKeywords.put("ltrmark", "\u200E");
    textKeywords.put("rdblquote", "\u201D");
    textKeywords.put("rquote", "\u2019");
    textKeywords.put("rtlmark", "\u200F");
    textKeywords.put("tab", "\u0009");
    textKeywords.put("zwj", "\u200D");
    textKeywords.put("zwnj", "\u200C");

      /* There is no Unicode equivalent to an optional hyphen, as far as
         I can tell. */
    textKeywords.put("-", "\u2027");  /* TODO: optional hyphen */
  }

  /* some entries in parserState */
  static final String TabAlignmentKey = "tab_alignment";
  static final String TabLeaderKey = "tab_leader";

  static Dictionary<String, char[]> characterSets;
  static boolean useNeXTForAnsi = false;

  static {
    characterSets = new Hashtable<String, char[]>();
  }

/* TODO: per-font font encodings ( \fcharset control word ) ? */

  /**
   * Creates a new RTFReader instance. Text will be sent to
   * the specified TextAcceptor.
   *
   * @param destination The TextAcceptor which is to receive the text.
   */
  public RTFReader(StyledDocument destination) {
    int i;

    target = destination;
    parserState = new Hashtable<Object, Object>();
    fontTable = new Hashtable<Integer, String>();

    rtfversion = -1;

    mockery = new MockAttributeSet();
    documentAttributes = new SimpleAttributeSet();
  }

  /**
   * Called when the RTFParser encounters a bin keyword in the
   * RTF stream.
   *
   * @see RTFParser
   */
  public void handleBinaryBlob(byte[] data) {
    if (skippingCharacters > 0) {
        /* a blob only counts as one character for skipping purposes */
      skippingCharacters--;
      return;
    }

    /* someday, someone will want to do something with blobs */
  }


  /**
   * Handles any pure text (containing no control characters) in the input
   * stream. Called by the superclass.
   */
  public void handleText(String text) {
    if (skippingCharacters > 0) {
      if (skippingCharacters >= text.length()) {
        skippingCharacters -= text.length();
        return;
      } else {
        text = text.substring(skippingCharacters);
        skippingCharacters = 0;
      }
    }

    if (rtfDestination != null) {
      rtfDestination.handleText(text);
      return;
    }

    warning("Text with no destination. oops.");
  }

  /**
   * The default color for text which has no specified color.
   */
  Color defaultColor() {
    return Color.black;
  }

  /**
   * Called by the superclass when a new RTF group is begun.
   * This implementation saves the current <code>parserState</code>, and gives
   * the current destination a chance to save its own state.
   *
   * @see RTFParser#begingroup
   */
  public void begingroup() {
    if (skippingCharacters > 0) {
        /* TODO this indicates an error in the RTF. Log it? */
      skippingCharacters = 0;
    }

    /* we do this little dance to avoid cloning the entire state stack and
       immediately throwing it away. */
    Object oldSaveState = parserState.get("_savedState");
    if (oldSaveState != null) {
      parserState.remove("_savedState");
    }
    Dictionary<String, Object> saveState = (Dictionary<String, Object>) ((Hashtable) parserState)
        .clone();
    if (oldSaveState != null) {
      saveState.put("_savedState", oldSaveState);
    }
    parserState.put("_savedState", saveState);

    if (rtfDestination != null) {
      rtfDestination.begingroup();
    }
  }

  /**
   * Called by the superclass when the current RTF group is closed.
   * This restores the parserState saved by <code>begingroup()</code>
   * as well as invoking the endgroup method of the current
   * destination.
   *
   * @see RTFParser#endgroup
   */
  public void endgroup() {
    if (skippingCharacters > 0) {
        /* NB this indicates an error in the RTF. Log it? */
      skippingCharacters = 0;
    }

    Dictionary<Object, Object> restoredState = (Dictionary<Object, Object>) parserState
        .get("_savedState");
    Destination restoredDestination = (Destination) restoredState.get("dst");
    if (restoredDestination != rtfDestination) {
      rtfDestination.close(); /* allow the destination to clean up */
      rtfDestination = restoredDestination;
    }
    Dictionary oldParserState = parserState;
    parserState = restoredState;
    if (rtfDestination != null) {
      rtfDestination.endgroup(oldParserState);
    }
  }

  protected void setRTFDestination(Destination newDestination) {
    /* Check that setting the destination won't close the
       current destination (should never happen) */
    Dictionary previousState = (Dictionary) parserState.get("_savedState");
    if (previousState != null) {
      if (rtfDestination != previousState.get("dst")) {
        warning("Warning, RTF destination overridden, invalid RTF.");
        rtfDestination.close();
      }
    }
    rtfDestination = newDestination;
    parserState.put("dst", rtfDestination);
  }

  /**
   * Called by the user when there is no more input (<i>i.e.</i>,
   * at the end of the RTF file.)
   *
   * @see OutputStream#close
   */
  public void close()
      throws IOException {
    Enumeration docProps = documentAttributes.getAttributeNames();
    while (docProps.hasMoreElements()) {
      Object propName = docProps.nextElement();
      target.putProperty(propName,
          documentAttributes.getAttribute(propName));
    }

    /* RTFParser should have ensured that all our groups are closed */

    warning("RTF filter done.");

    super.close();
  }

  /**
   * Handles a parameterless RTF keyword. This is called by the superclass
   * (RTFParser) when a keyword is found in the input stream.
   *
   * @returns <code>true</code> if the keyword is recognized and handled; <code>false</code>
   * otherwise
   * @see RTFParser#handleKeyword
   */
  public boolean handleKeyword(String keyword) {
    String item;
    boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;

    if (skippingCharacters > 0) {
      skippingCharacters--;
      return true;
    }

    ignoreGroupIfUnknownKeyword = false;

    if ((item = textKeywords.get(keyword)) != null) {
      handleText(item);
      return true;
    }

    if (keyword.equals("fonttbl")) {
      setRTFDestination(new FonttblDestination());
      return true;
    }

    if (keyword.equals("colortbl")) {
      setRTFDestination(new ColortblDestination());
      return true;
    }

    if (keyword.equals("stylesheet")) {
      setRTFDestination(new StylesheetDestination());
      return true;
    }

    if (keyword.equals("info")) {
      setRTFDestination(new InfoDestination());
      return false;
    }

    if (keyword.equals("mac")) {
      setCharacterSet("mac");
      return true;
    }

    if (keyword.equals("ansi")) {
      if (useNeXTForAnsi) {
        setCharacterSet("NeXT");
      } else {
        setCharacterSet("ansi");
      }
      return true;
    }

    if (keyword.equals("next")) {
      setCharacterSet("NeXT");
      return true;
    }

    if (keyword.equals("pc")) {
      setCharacterSet("cpg437"); /* IBM Code Page 437 */
      return true;
    }

    if (keyword.equals("pca")) {
      setCharacterSet("cpg850"); /* IBM Code Page 850 */
      return true;
    }

    if (keyword.equals("*")) {
      ignoreGroupIfUnknownKeyword = true;
      return true;
    }

    if (rtfDestination != null) {
      if (rtfDestination.handleKeyword(keyword)) {
        return true;
      }
    }

    /* this point is reached only if the keyword is unrecognized */

    /* other destinations we don't understand and therefore ignore */
    if (keyword.equals("aftncn") ||
        keyword.equals("aftnsep") ||
        keyword.equals("aftnsepc") ||
        keyword.equals("annotation") ||
        keyword.equals("atnauthor") ||
        keyword.equals("atnicn") ||
        keyword.equals("atnid") ||
        keyword.equals("atnref") ||
        keyword.equals("atntime") ||
        keyword.equals("atrfend") ||
        keyword.equals("atrfstart") ||
        keyword.equals("bkmkend") ||
        keyword.equals("bkmkstart") ||
        keyword.equals("datafield") ||
        keyword.equals("do") ||
        keyword.equals("dptxbxtext") ||
        keyword.equals("falt") ||
        keyword.equals("field") ||
        keyword.equals("file") ||
        keyword.equals("filetbl") ||
        keyword.equals("fname") ||
        keyword.equals("fontemb") ||
        keyword.equals("fontfile") ||
        keyword.equals("footer") ||
        keyword.equals("footerf") ||
        keyword.equals("footerl") ||
        keyword.equals("footerr") ||
        keyword.equals("footnote") ||
        keyword.equals("ftncn") ||
        keyword.equals("ftnsep") ||
        keyword.equals("ftnsepc") ||
        keyword.equals("header") ||
        keyword.equals("headerf") ||
        keyword.equals("headerl") ||
        keyword.equals("headerr") ||
        keyword.equals("keycode") ||
        keyword.equals("nextfile") ||
        keyword.equals("object") ||
        keyword.equals("pict") ||
        keyword.equals("pn") ||
        keyword.equals("pnseclvl") ||
        keyword.equals("pntxtb") ||
        keyword.equals("pntxta") ||
        keyword.equals("revtbl") ||
        keyword.equals("rxe") ||
        keyword.equals("tc") ||
        keyword.equals("template") ||
        keyword.equals("txe") ||
        keyword.equals("xe")) {
      ignoreGroupIfUnknownKeywordSave = true;
    }

    if (ignoreGroupIfUnknownKeywordSave) {
      setRTFDestination(new DiscardingDestination());
    }

    return false;
  }

  /**
   * Handles an RTF keyword and its integer parameter.
   * This is called by the superclass
   * (RTFParser) when a keyword is found in the input stream.
   *
   * @returns <code>true</code> if the keyword is recognized and handled; <code>false</code>
   * otherwise
   * @see RTFParser#handleKeyword
   */
  public boolean handleKeyword(String keyword, int parameter) {
    boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;

    if (skippingCharacters > 0) {
      skippingCharacters--;
      return true;
    }

    ignoreGroupIfUnknownKeyword = false;

    if (keyword.equals("uc")) {
        /* count of characters to skip after a unicode character */
      parserState.put("UnicodeSkip", Integer.valueOf(parameter));
      return true;
    }
    if (keyword.equals("u")) {
      if (parameter < 0) {
        parameter = parameter + 65536;
      }
      handleText((char) parameter);
      Number skip = (Number) (parserState.get("UnicodeSkip"));
      if (skip != null) {
        skippingCharacters = skip.intValue();
      } else {
        skippingCharacters = 1;
      }
      return true;
    }

    if (keyword.equals("rtf")) {
      rtfversion = parameter;
      setRTFDestination(new DocumentDestination());
      return true;
    }

    if (keyword.startsWith("NeXT") ||
        keyword.equals("private")) {
      ignoreGroupIfUnknownKeywordSave = true;
    }

    if (rtfDestination != null) {
      if (rtfDestination.handleKeyword(keyword, parameter)) {
        return true;
      }
    }

    /* this point is reached only if the keyword is unrecognized */

    if (ignoreGroupIfUnknownKeywordSave) {
      setRTFDestination(new DiscardingDestination());
    }

    return false;
  }

  private void setTargetAttribute(String name, Object value) {
//    target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name)));
  }

  /**
   * setCharacterSet sets the current translation table to correspond with
   * the named character set. The character set is loaded if necessary.
   *
   * @see AbstractFilter
   */
  public void setCharacterSet(String name) {
    Object set;

    try {
      set = getCharacterSet(name);
    } catch (Exception e) {
      warning("Exception loading RTF character set \"" + name + "\": " + e);
      set = null;
    }

    if (set != null) {
      translationTable = (char[]) set;
    } else {
      warning("Unknown RTF character set \"" + name + "\"");
      if (!name.equals("ansi")) {
        try {
          translationTable = (char[]) getCharacterSet("ansi");
        } catch (IOException e) {
          throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")",
              e);
        }
      }
    }

    setTargetAttribute(Constants.RTFCharacterSet, name);
  }

  /**
   * Adds a character set to the RTFReader's list
   * of known character sets
   */
  public static void
  defineCharacterSet(String name, char[] table) {
    if (table.length < 256) {
      throw new IllegalArgumentException("Translation table must have 256 entries.");
    }
    characterSets.put(name, table);
  }

  /**
   * Looks up a named character set. A character set is a 256-entry
   * array of characters, mapping unsigned byte values to their Unicode
   * equivalents. The character set is loaded if necessary.
   *
   * @returns the character set
   */
  public static Object
  getCharacterSet(final String name)
      throws IOException {
    char[] set = characterSets.get(name);
    if (set == null) {
      InputStream charsetStream = AccessController.doPrivileged(
          new PrivilegedAction<InputStream>() {
            public InputStream run() {
              return RTFReader.class.getResourceAsStream("charsets/" + name + ".txt");
            }
          });
      set = readCharset(charsetStream);
      defineCharacterSet(name, set);
    }
    return set;
  }

  /**
   * Parses a character set from an InputStream. The character set
   * must contain 256 decimal integers, separated by whitespace, with
   * no punctuation. B- and C- style comments are allowed.
   *
   * @returns the newly read character set
   */
  static char[] readCharset(InputStream strm)
      throws IOException {
    char[] values = new char[256];
    int i;
    StreamTokenizer in = new StreamTokenizer(new BufferedReader(
        new InputStreamReader(strm, "ISO-8859-1")));

    in.eolIsSignificant(false);
    in.commentChar('#');
    in.slashSlashComments(true);
    in.slashStarComments(true);

    i = 0;
    while (i < 256) {
      int ttype;
      try {
        ttype = in.nextToken();
      } catch (Exception e) {
        throw new IOException("Unable to read from character set file (" + e + ")");
      }
      if (ttype != in.TT_NUMBER) {
//          System.out.println("Bad token: type=" + ttype + " tok=" + in.sval);
        throw new IOException("Unexpected token in character set file");
//          continue;
      }
      values[i] = (char) (in.nval);
      i++;
    }

    return values;
  }

  static char[] readCharset(java.net.URL href)
      throws IOException {
    return readCharset(href.openStream());
  }

  /**
   * An interface (could be an entirely abstract class) describing
   * a destination. The RTF reader always has a current destination
   * which is where text is sent.
   *
   * @see RTFReader
   */
  interface Destination {

    void handleBinaryBlob(byte[] data);

    void handleText(String text);

    boolean handleKeyword(String keyword);

    boolean handleKeyword(String keyword, int parameter);

    void begingroup();

    void endgroup(Dictionary oldState);

    void close();
  }

  /**
   * This data-sink class is used to implement ignored destinations
   * (e.g. {\*\blegga blah blah blah} )
   * It accepts all keywords and text but does nothing with them.
   */
  class DiscardingDestination implements Destination {

    public void handleBinaryBlob(byte[] data) {
        /* Discard binary blobs. */
    }

    public void handleText(String text) {
        /* Discard text. */
    }

    public boolean handleKeyword(String text) {
        /* Accept and discard keywords. */
      return true;
    }

    public boolean handleKeyword(String text, int parameter) {
        /* Accept and discard parameterized keywords. */
      return true;
    }

    public void begingroup() {
        /* Ignore groups --- the RTFReader will keep track of the
           current group level as necessary */
    }

    public void endgroup(Dictionary oldState) {
        /* Ignore groups */
    }

    public void close() {
        /* No end-of-destination cleanup needed */
    }
  }

  /**
   * Reads the fonttbl group, inserting fonts into the RTFReader's
   * fontTable dictionary.
   */
  class FonttblDestination implements Destination {

    int nextFontNumber;
    Integer fontNumberKey = null;
    String nextFontFamily;

    public void handleBinaryBlob(byte[] data) { /* Discard binary blobs. */ }

    public void handleText(String text) {
      int semicolon = text.indexOf(';');
      String fontName;

      if (semicolon > -1) {
        fontName = text.substring(0, semicolon);
      } else {
        fontName = text;
      }


        /* TODO: do something with the font family. */

      if (nextFontNumber == -1
          && fontNumberKey != null) {
        //font name might be broken across multiple calls
        fontName = fontTable.get(fontNumberKey) + fontName;
      } else {
        fontNumberKey = Integer.valueOf(nextFontNumber);
      }
      fontTable.put(fontNumberKey, fontName);

      nextFontNumber = -1;
      nextFontFamily = null;
    }

    public boolean handleKeyword(String keyword) {
      if (keyword.charAt(0) == 'f') {
        nextFontFamily = keyword.substring(1);
        return true;
      }

      return false;
    }

    public boolean handleKeyword(String keyword, int parameter) {
      if (keyword.equals("f")) {
        nextFontNumber = parameter;
        return true;
      }

      return false;
    }

    /* Groups are irrelevant. */
    public void begingroup() {
    }

    public void endgroup(Dictionary oldState) {
    }

    /* currently, the only thing we do when the font table ends is
       dump its contents to the debugging log. */
    public void close() {
      Enumeration<Integer> nums = fontTable.keys();
      warning("Done reading font table.");
      while (nums.hasMoreElements()) {
        Integer num = nums.nextElement();
        warning("Number " + num + ": " + fontTable.get(num));
      }
    }
  }

  /**
   * Reads the colortbl group. Upon end-of-group, the RTFReader's
   * color table is set to an array containing the read colors.
   */
  class ColortblDestination implements Destination {

    int red, green, blue;
    Vector<Color> proTemTable;

    public ColortblDestination() {
      red = 0;
      green = 0;
      blue = 0;
      proTemTable = new Vector<Color>();
    }

    public void handleText(String text) {
      int index;

      for (index = 0; index < text.length(); index++) {
        if (text.charAt(index) == ';') {
          Color newColor;
          newColor = new Color(red, green, blue);
          proTemTable.addElement(newColor);
        }
      }
    }

    public void close() {
      int count = proTemTable.size();
      warning("Done reading color table, " + count + " entries.");
      colorTable = new Color[count];
      proTemTable.copyInto(colorTable);
    }

    public boolean handleKeyword(String keyword, int parameter) {
      if (keyword.equals("red")) {
        red = parameter;
      } else if (keyword.equals("green")) {
        green = parameter;
      } else if (keyword.equals("blue")) {
        blue = parameter;
      } else {
        return false;
      }

      return true;
    }

    /* Colortbls don't understand any parameterless keywords */
    public boolean handleKeyword(String keyword) {
      return false;
    }

    /* Groups are irrelevant. */
    public void begingroup() {
    }

    public void endgroup(Dictionary oldState) {
    }

    /* Shouldn't see any binary blobs ... */
    public void handleBinaryBlob(byte[] data) {
    }
  }

  /**
   * Handles the stylesheet keyword. Styles are read and sorted
   * into the three style arrays in the RTFReader.
   */
  class StylesheetDestination
      extends DiscardingDestination
      implements Destination {

    Dictionary<Integer, StyleDefiningDestination> definedStyles;

    public StylesheetDestination() {
      definedStyles = new Hashtable<Integer, StyleDefiningDestination>();
    }

    public void begingroup() {
      setRTFDestination(new StyleDefiningDestination());
    }

    public void close() {
      Vector<Style> chrStyles = new Vector<Style>();
      Vector<Style> pgfStyles = new Vector<Style>();
      Vector<Style> secStyles = new Vector<Style>();
      Enumeration<StyleDefiningDestination> styles = definedStyles.elements();
      while (styles.hasMoreElements()) {
        StyleDefiningDestination style;
        Style defined;
        style = styles.nextElement();
        defined = style.realize();
        warning("Style " + style.number + " (" + style.styleName + "): " + defined);
        String stype = (String) defined.getAttribute(Constants.StyleType);
        Vector<Style> toSet;
        if (stype.equals(Constants.STSection)) {
          toSet = secStyles;
        } else if (stype.equals(Constants.STCharacter)) {
          toSet = chrStyles;
        } else {
          toSet = pgfStyles;
        }
        if (toSet.size() <= style.number) {
          toSet.setSize(style.number + 1);
        }
        toSet.setElementAt(defined, style.number);
      }
      if (!(chrStyles.isEmpty())) {
        Style[] styleArray = new Style[chrStyles.size()];
        chrStyles.copyInto(styleArray);
        characterStyles = styleArray;
      }
      if (!(pgfStyles.isEmpty())) {
        Style[] styleArray = new Style[pgfStyles.size()];
        pgfStyles.copyInto(styleArray);
        paragraphStyles = styleArray;
      }
      if (!(secStyles.isEmpty())) {
        Style[] styleArray = new Style[secStyles.size()];
        secStyles.copyInto(styleArray);
        sectionStyles = styleArray;
      }

/* (old debugging code)
        int i, m;
        if (characterStyles != null) {
          m = characterStyles.length;
          for(i=0;i<m;i++)
            warnings.println("chrStyle["+i+"]="+characterStyles[i]);
        } else warnings.println("No character styles.");
        if (paragraphStyles != null) {
          m = paragraphStyles.length;
          for(i=0;i<m;i++)
            warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]);
        } else warnings.println("No paragraph styles.");
        if (sectionStyles != null) {
          m = characterStyles.length;
          for(i=0;i<m;i++)
            warnings.println("secStyle["+i+"]="+sectionStyles[i]);
        } else warnings.println("No section styles.");
*/
    }

    /**
     * This subclass handles an individual style
     */
    class StyleDefiningDestination
        extends AttributeTrackingDestination
        implements Destination {

      final int STYLENUMBER_NONE = 222;
      boolean additive;
      boolean characterStyle;
      boolean sectionStyle;
      public String styleName;
      public int number;
      int basedOn;
      int nextStyle;
      boolean hidden;

      Style realizedStyle;

      public StyleDefiningDestination() {
        additive = false;
        characterStyle = false;
        sectionStyle = false;
        styleName = null;
        number = 0;
        basedOn = STYLENUMBER_NONE;
        nextStyle = STYLENUMBER_NONE;
        hidden = false;
      }

      public void handleText(String text) {
        if (styleName != null) {
          styleName = styleName + text;
        } else {
          styleName = text;
        }
      }

      public void close() {
        int semicolon = (styleName == null) ? 0 : styleName.indexOf(';');
        if (semicolon > 0) {
          styleName = styleName.substring(0, semicolon);
        }
        definedStyles.put(Integer.valueOf(number), this);
        super.close();
      }

      public boolean handleKeyword(String keyword) {
        if (keyword.equals("additive")) {
          additive = true;
          return true;
        }
        if (keyword.equals("shidden")) {
          hidden = true;
          return true;
        }
        return super.handleKeyword(keyword);
      }

      public boolean handleKeyword(String keyword, int parameter) {
        if (keyword.equals("s")) {
          characterStyle = false;
          sectionStyle = false;
          number = parameter;
        } else if (keyword.equals("cs")) {
          characterStyle = true;
          sectionStyle = false;
          number = parameter;
        } else if (keyword.equals("ds")) {
          characterStyle = false;
          sectionStyle = true;
          number = parameter;
        } else if (keyword.equals("sbasedon")) {
          basedOn = parameter;
        } else if (keyword.equals("snext")) {
          nextStyle = parameter;
        } else {
          return super.handleKeyword(keyword, parameter);
        }
        return true;
      }

      public Style realize() {
        Style basis = null;
        Style next = null;

        if (realizedStyle != null) {
          return realizedStyle;
        }

        if (basedOn != STYLENUMBER_NONE) {
          StyleDefiningDestination styleDest;
          styleDest = definedStyles.get(Integer.valueOf(basedOn));
          if (styleDest != null && styleDest != this) {
            basis = styleDest.realize();
          }
        }

            /* NB: Swing StyleContext doesn't allow distinct styles with
               the same name; RTF apparently does. This may confuse the
               user. */
        realizedStyle = target.addStyle(styleName, basis);

        if (characterStyle) {
          realizedStyle.addAttributes(currentTextAttributes());
          realizedStyle.addAttribute(Constants.StyleType,
              Constants.STCharacter);
        } else if (sectionStyle) {
          realizedStyle.addAttributes(currentSectionAttributes());
          realizedStyle.addAttribute(Constants.StyleType,
              Constants.STSection);
        } else { /* must be a paragraph style */
          realizedStyle.addAttributes(currentParagraphAttributes());
          realizedStyle.addAttribute(Constants.StyleType,
              Constants.STParagraph);
        }

        if (nextStyle != STYLENUMBER_NONE) {
          StyleDefiningDestination styleDest;
          styleDest = definedStyles.get(Integer.valueOf(nextStyle));
          if (styleDest != null) {
            next = styleDest.realize();
          }
        }

        if (next != null) {
          realizedStyle.addAttribute(Constants.StyleNext, next);
        }
        realizedStyle.addAttribute(Constants.StyleAdditive,
            Boolean.valueOf(additive));
        realizedStyle.addAttribute(Constants.StyleHidden,
            Boolean.valueOf(hidden));

        return realizedStyle;
      }
    }
  }

  /**
   * Handles the info group. Currently no info keywords are recognized
   * so this is a subclass of DiscardingDestination.
   */
  class InfoDestination
      extends DiscardingDestination
      implements Destination {

  }

  /**
   * RTFReader.TextHandlingDestination is an abstract RTF destination
   * which simply tracks the attributes specified by the RTF control words
   * in internal form and can produce acceptable AttributeSets for the
   * current character, paragraph, and section attributes. It is up
   * to the subclasses to determine what is done with the actual text.
   */
  abstract class AttributeTrackingDestination implements Destination {

    /**
     * This is the "chr" element of parserState, cached for
     * more efficient use
     */
    MutableAttributeSet characterAttributes;
    /**
     * This is the "pgf" element of parserState, cached for
     * more efficient use
     */
    MutableAttributeSet paragraphAttributes;
    /**
     * This is the "sec" element of parserState, cached for
     * more efficient use
     */
    MutableAttributeSet sectionAttributes;

    public AttributeTrackingDestination() {
      characterAttributes = rootCharacterAttributes();
      parserState.put("chr", characterAttributes);
      paragraphAttributes = rootParagraphAttributes();
      parserState.put("pgf", paragraphAttributes);
      sectionAttributes = rootSectionAttributes();
      parserState.put("sec", sectionAttributes);
    }

    abstract public void handleText(String text);

    public void handleBinaryBlob(byte[] data) {
        /* This should really be in TextHandlingDestination, but
         * since *nobody* does anything with binary blobs, this
         * is more convenient. */
      warning("Unexpected binary data in RTF file.");
    }

    public void begingroup() {
      AttributeSet characterParent = currentTextAttributes();
      AttributeSet paragraphParent = currentParagraphAttributes();
      AttributeSet sectionParent = currentSectionAttributes();

        /* It would probably be more efficient to use the
         * resolver property of the attributes set for
         * implementing rtf groups,
         * but that's needed for styles. */

        /* update the cached attribute dictionaries */
      characterAttributes = new SimpleAttributeSet();
      characterAttributes.addAttributes(characterParent);
      parserState.put("chr", characterAttributes);

      paragraphAttributes = new SimpleAttributeSet();
      paragraphAttributes.addAttributes(paragraphParent);
      parserState.put("pgf", paragraphAttributes);

      sectionAttributes = new SimpleAttributeSet();
      sectionAttributes.addAttributes(sectionParent);
      parserState.put("sec", sectionAttributes);
    }

    public void endgroup(Dictionary oldState) {
      characterAttributes = (MutableAttributeSet) parserState.get("chr");
      paragraphAttributes = (MutableAttributeSet) parserState.get("pgf");
      sectionAttributes = (MutableAttributeSet) parserState.get("sec");
    }

    public void close() {
    }

    public boolean handleKeyword(String keyword) {
      if (keyword.equals("ulnone")) {
        return handleKeyword("ul", 0);
      }

      {
        RTFAttribute attr = straightforwardAttributes.get(keyword);
        if (attr != null) {
          boolean ok;

          switch (attr.domain()) {
            case RTFAttribute.D_CHARACTER:
              ok = attr.set(characterAttributes);
              break;
            case RTFAttribute.D_PARAGRAPH:
              ok = attr.set(paragraphAttributes);
              break;
            case RTFAttribute.D_SECTION:
              ok = attr.set(sectionAttributes);
              break;
            case RTFAttribute.D_META:
              mockery.backing = parserState;
              ok = attr.set(mockery);
              mockery.backing = null;
              break;
            case RTFAttribute.D_DOCUMENT:
              ok = attr.set(documentAttributes);
              break;
            default:
                    /* should never happen */
              ok = false;
              break;
          }
          if (ok) {
            return true;
          }
        }
      }

      if (keyword.equals("plain")) {
        resetCharacterAttributes();
        return true;
      }

      if (keyword.equals("pard")) {
        resetParagraphAttributes();
        return true;
      }

      if (keyword.equals("sectd")) {
        resetSectionAttributes();
        return true;
      }

      return false;
    }

    public boolean handleKeyword(String keyword, int parameter) {
      boolean booleanParameter = (parameter != 0);

      if (keyword.equals("fc")) {
        keyword = "cf"; /* whatEVER, dude. */
      }

      if (keyword.equals("f")) {
        parserState.put(keyword, Integer.valueOf(parameter));
        return true;
      }
      if (keyword.equals("cf")) {
        parserState.put(keyword, Integer.valueOf(parameter));
        return true;
      }

      {
        RTFAttribute attr = straightforwardAttributes.get(keyword);
        if (attr != null) {
          boolean ok;

          switch (attr.domain()) {
            case RTFAttribute.D_CHARACTER:
              ok = attr.set(characterAttributes, parameter);
              break;
            case RTFAttribute.D_PARAGRAPH:
              ok = attr.set(paragraphAttributes, parameter);
              break;
            case RTFAttribute.D_SECTION:
              ok = attr.set(sectionAttributes, parameter);
              break;
            case RTFAttribute.D_META:
              mockery.backing = parserState;
              ok = attr.set(mockery, parameter);
              mockery.backing = null;
              break;
            case RTFAttribute.D_DOCUMENT:
              ok = attr.set(documentAttributes, parameter);
              break;
            default:
                    /* should never happen */
              ok = false;
              break;
          }
          if (ok) {
            return true;
          }
        }
      }

      if (keyword.equals("fs")) {
        StyleConstants.setFontSize(characterAttributes, (parameter / 2));
        return true;
      }

        /* TODO: superscript/subscript */

      if (keyword.equals("sl")) {
        if (parameter == 1000) {  /* magic value! */
          characterAttributes.removeAttribute(StyleConstants.LineSpacing);
        } else {
                /* TODO: The RTF sl attribute has special meaning if it's
                   negative. Make sure that SwingText has the same special
                   meaning, or find a way to imitate that. When SwingText
                   handles this, also recognize the slmult keyword. */
          StyleConstants.setLineSpacing(characterAttributes,
              parameter / 20f);
        }
        return true;
      }

        /* TODO: Other kinds of underlining */

      if (keyword.equals("tx") || keyword.equals("tb")) {
        float tabPosition = parameter / 20f;
        int tabAlignment, tabLeader;
        Number item;

        tabAlignment = TabStop.ALIGN_LEFT;
        item = (Number) (parserState.get("tab_alignment"));
        if (item != null) {
          tabAlignment = item.intValue();
        }
        tabLeader = TabStop.LEAD_NONE;
        item = (Number) (parserState.get("tab_leader"));
        if (item != null) {
          tabLeader = item.intValue();
        }
        if (keyword.equals("tb")) {
          tabAlignment = TabStop.ALIGN_BAR;
        }

        parserState.remove("tab_alignment");
        parserState.remove("tab_leader");

        TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader);
        Dictionary<Object, Object> tabs;
        Integer stopCount;

        tabs = (Dictionary<Object, Object>) parserState.get("_tabs");
        if (tabs == null) {
          tabs = new Hashtable<Object, Object>();
          parserState.put("_tabs", tabs);
          stopCount = Integer.valueOf(1);
        } else {
          stopCount = (Integer) tabs.get("stop count");
          stopCount = Integer.valueOf(1 + stopCount.intValue());
        }
        tabs.put(stopCount, newStop);
        tabs.put("stop count", stopCount);
        parserState.remove("_tabs_immutable");

        return true;
      }

      if (keyword.equals("s") &&
          paragraphStyles != null) {
        parserState.put("paragraphStyle", paragraphStyles[parameter]);
        return true;
      }

      if (keyword.equals("cs") &&
          characterStyles != null) {
        parserState.put("characterStyle", characterStyles[parameter]);
        return true;
      }

      if (keyword.equals("ds") &&
          sectionStyles != null) {
        parserState.put("sectionStyle", sectionStyles[parameter]);
        return true;
      }

      return false;
    }

    /**
     * Returns a new MutableAttributeSet containing the
     * default character attributes
     */
    protected MutableAttributeSet rootCharacterAttributes() {
      MutableAttributeSet set = new SimpleAttributeSet();

        /* TODO: default font */

      StyleConstants.setItalic(set, false);
      StyleConstants.setBold(set, false);
      StyleConstants.setUnderline(set, false);
      StyleConstants.setForeground(set, defaultColor());

      return set;
    }

    /**
     * Returns a new MutableAttributeSet containing the
     * default paragraph attributes
     */
    protected MutableAttributeSet rootParagraphAttributes() {
      MutableAttributeSet set = new SimpleAttributeSet();

      StyleConstants.setLeftIndent(set, 0f);
      StyleConstants.setRightIndent(set, 0f);
      StyleConstants.setFirstLineIndent(set, 0f);

        /* TODO: what should this be, really? */
      set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE));

      return set;
    }

    /**
     * Returns a new MutableAttributeSet containing the
     * default section attributes
     */
    protected MutableAttributeSet rootSectionAttributes() {
      MutableAttributeSet set = new SimpleAttributeSet();

      return set;
    }

    /**
     * Calculates the current text (character) attributes in a form suitable
     * for SwingText from the current parser state.
     *
     * @returns a new MutableAttributeSet containing the text attributes.
     */
    MutableAttributeSet currentTextAttributes() {
      MutableAttributeSet attributes =
          new SimpleAttributeSet(characterAttributes);
      Integer fontnum;
      Integer stateItem;

        /* figure out the font name */
        /* TODO: catch exceptions for undefined attributes,
           bad font indices, etc.? (as it stands, it is the caller's
           job to clean up after corrupt RTF) */
      fontnum = (Integer) parserState.get("f");
        /* note setFontFamily() can not handle a null font */
      String fontFamily;
      if (fontnum != null) {
        fontFamily = fontTable.get(fontnum);
      } else {
        fontFamily = null;
      }
      if (fontFamily != null) {
        StyleConstants.setFontFamily(attributes, fontFamily);
      } else {
        attributes.removeAttribute(StyleConstants.FontFamily);
      }

      if (colorTable != null) {
        stateItem = (Integer) parserState.get("cf");
        if (stateItem != null) {
          Color fg = colorTable[stateItem.intValue()];
          StyleConstants.setForeground(attributes, fg);
        } else {
                /* AttributeSet dies if you set a value to null */
          attributes.removeAttribute(StyleConstants.Foreground);
        }
      }

      if (colorTable != null) {
        stateItem = (Integer) parserState.get("cb");
        if (stateItem != null) {
          Color bg = colorTable[stateItem.intValue()];
          attributes.addAttribute(StyleConstants.Background,
              bg);
        } else {
                /* AttributeSet dies if you set a value to null */
          attributes.removeAttribute(StyleConstants.Background);
        }
      }

      Style characterStyle = (Style) parserState.get("characterStyle");
      if (characterStyle != null) {
        attributes.setResolveParent(characterStyle);
      }

        /* Other attributes are maintained directly in "attributes" */

      return attributes;
    }

    /**
     * Calculates the current paragraph attributes (with keys
     * as given in StyleConstants) from the current parser state.
     *
     * @returns a newly created MutableAttributeSet.
     * @see StyleConstants
     */
    MutableAttributeSet currentParagraphAttributes() {
        /* NB if there were a mutableCopy() method we should use it */
      MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes);

      Integer stateItem;

      /*** Tab stops ***/
      TabStop tabs[];

      tabs = (TabStop[]) parserState.get("_tabs_immutable");
      if (tabs == null) {
        Dictionary workingTabs = (Dictionary) parserState.get("_tabs");
        if (workingTabs != null) {
          int count = ((Integer) workingTabs.get("stop count")).intValue();
          tabs = new TabStop[count];
          for (int ix = 1; ix <= count; ix++) {
            tabs[ix - 1] = (TabStop) workingTabs.get(Integer.valueOf(ix));
          }
          parserState.put("_tabs_immutable", tabs);
        }
      }
      if (tabs != null) {
        bld.addAttribute(Constants.Tabs, tabs);
      }

      Style paragraphStyle = (Style) parserState.get("paragraphStyle");
      if (paragraphStyle != null) {
        bld.setResolveParent(paragraphStyle);
      }

      return bld;
    }

    /**
     * Calculates the current section attributes
     * from the current parser state.
     *
     * @returns a newly created MutableAttributeSet.
     */
    public AttributeSet currentSectionAttributes() {
      MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes);

      Style sectionStyle = (Style) parserState.get("sectionStyle");
      if (sectionStyle != null) {
        attributes.setResolveParent(sectionStyle);
      }

      return attributes;
    }

    /**
     * Resets the filter's internal notion of the current character
     * attributes to their default values. Invoked to handle the
     * \plain keyword.
     */
    protected void resetCharacterAttributes() {
      handleKeyword("f", 0);
      handleKeyword("cf", 0);

      handleKeyword("fs", 24);  /* 12 pt. */

      Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
      while (attributes.hasMoreElements()) {
        RTFAttribute attr = attributes.nextElement();
        if (attr.domain() == RTFAttribute.D_CHARACTER) {
          attr.setDefault(characterAttributes);
        }
      }

      handleKeyword("sl", 1000);

      parserState.remove("characterStyle");
    }

    /**
     * Resets the filter's internal notion of the current paragraph's
     * attributes to their default values. Invoked to handle the
     * \pard keyword.
     */
    protected void resetParagraphAttributes() {
      parserState.remove("_tabs");
      parserState.remove("_tabs_immutable");
      parserState.remove("paragraphStyle");

      StyleConstants.setAlignment(paragraphAttributes,
          StyleConstants.ALIGN_LEFT);

      Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
      while (attributes.hasMoreElements()) {
        RTFAttribute attr = attributes.nextElement();
        if (attr.domain() == RTFAttribute.D_PARAGRAPH) {
          attr.setDefault(characterAttributes);
        }
      }
    }

    /**
     * Resets the filter's internal notion of the current section's
     * attributes to their default values. Invoked to handle the
     * \sectd keyword.
     */
    protected void resetSectionAttributes() {
      Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
      while (attributes.hasMoreElements()) {
        RTFAttribute attr = attributes.nextElement();
        if (attr.domain() == RTFAttribute.D_SECTION) {
          attr.setDefault(characterAttributes);
        }
      }

      parserState.remove("sectionStyle");
    }
  }

  /**
   * RTFReader.TextHandlingDestination provides basic text handling
   * functionality. Subclasses must implement: <dl>
   * <dt>deliverText()<dd>to handle a run of text with the same
   * attributes
   * <dt>finishParagraph()<dd>to end the current paragraph and
   * set the paragraph's attributes
   * <dt>endSection()<dd>to end the current section
   * </dl>
   */
  abstract class TextHandlingDestination
      extends AttributeTrackingDestination
      implements Destination {

    /**
     * <code>true</code> if the reader has not just finished
     * a paragraph; false upon startup
     */
    boolean inParagraph;

    public TextHandlingDestination() {
      super();
      inParagraph = false;
    }

    public void handleText(String text) {
      if (!inParagraph) {
        beginParagraph();
      }

      deliverText(text, currentTextAttributes());
    }

    abstract void deliverText(String text, AttributeSet characterAttributes);

    public void close() {
      if (inParagraph) {
        endParagraph();
      }

      super.close();
    }

    public boolean handleKeyword(String keyword) {
      if (keyword.equals("\r") || keyword.equals("\n")) {
        keyword = "par";
      }

      if (keyword.equals("par")) {
//          warnings.println("Ending paragraph.");
        endParagraph();
        return true;
      }

      if (keyword.equals("sect")) {
//          warnings.println("Ending section.");
        endSection();
        return true;
      }

      return super.handleKeyword(keyword);
    }

    protected void beginParagraph() {
      inParagraph = true;
    }

    protected void endParagraph() {
      AttributeSet pgfAttributes = currentParagraphAttributes();
      AttributeSet chrAttributes = currentTextAttributes();
      finishParagraph(pgfAttributes, chrAttributes);
      inParagraph = false;
    }

    abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA);

    abstract void endSection();
  }

  /**
   * RTFReader.DocumentDestination is a concrete subclass of
   * TextHandlingDestination which appends the text to the
   * StyledDocument given by the <code>target</code> ivar of the
   * containing RTFReader.
   */
  class DocumentDestination
      extends TextHandlingDestination
      implements Destination {

    public void deliverText(String text, AttributeSet characterAttributes) {
      try {
        target.insertString(target.getLength(),
            text,
            currentTextAttributes());
      } catch (BadLocationException ble) {
            /* This shouldn't be able to happen, of course */
            /* TODO is InternalError the correct error to throw? */
        throw new InternalError(ble.getMessage(), ble);
      }
    }

    public void finishParagraph(AttributeSet pgfAttributes,
        AttributeSet chrAttributes) {
      int pgfEndPosition = target.getLength();
      try {
        target.insertString(pgfEndPosition, "\n", chrAttributes);
        target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true);
      } catch (BadLocationException ble) {
            /* This shouldn't be able to happen, of course */
            /* TODO is InternalError the correct error to throw? */
        throw new InternalError(ble.getMessage(), ble);
      }
    }

    public void endSection() {
        /* If we implemented sections, we'd end 'em here */
    }
  }

}
